const MimeNode = require('./mime-node');
const mimeFuncs = require('./mime-funcs');

/**
 * Creates the object for composing a MimeNode instance out from the mail options
 *
 * @constructor
 * @param {Object} mail Mail options
 */
class MailComposer {
  constructor(mail) {
    //mail:{
    // from:'xxx', 发送方
    // to:'xxx', 接收方
    // subject:'xxx', 标题
    // content:'xxx', 内容
    // headers:{},
    //}
    this.mail = mail || {};
    this.message = false;
  }

  /**
   * Builds MimeNode instance
   */
  //新建 mimeNode 实例
  compile() {
    //{
    // content:'xxxx',
    // contentTransferEncoding:undefined,
    // contentType:'text/html;charset=utf-8',
    // }
    this._alternatives = this.getAlternatives();
    //value 同 _alternatives
    this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
    //{attached:[].related:[]}
    this._attachments = this.getAttachments(!!this._htmlNode);

    this._useRelated = !!(this._htmlNode && this._attachments.related.length);

    this._useAlternative = this._alternatives.length > 1;

    this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);

    // Compose MIME tree
    if (this.mail.raw) {
      this.message = new MimeNode().setRaw(this.mail.raw);
    } else if (this._useMixed) {
      this.message = this._createMixed();
    } else if (this._useAlternative) {
      this.message = this._createAlternative();
    } else if (this._useRelated) {
      this.message = this._createRelated();
    } else {
      this.message = this._createContentNode(
        false,
        []
          .concat(this._alternatives || [])
          .concat(this._attachments.attached || [])
          .shift() || {
          contentType: 'text/plain',
          content: ''
        }
      );
    }

    // Add custom headers
    if (this.mail.headers) {
      this.message.addHeader(this.mail.headers);
    }

    // Add headers to the root node, always overrides custom headers
    ['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
      let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
      if (this.mail[key]) {
        this.message.setHeader(header, this.mail[key]);
      }
    });

    // ensure Message-Id value
    this.message.messageId();
    //即 mimeNode 实例
    return this.message;
  }

  /**
   * List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
   *
   * @param {Boolean} findRelated If true separate related attachments from attached ones
   * @returns {Object} An object of arrays (`related` and `attached`)
   */
  getAttachments(findRelated) {
    // let icalEvent, eventObject;
    // let attachments = []

      return {
        // attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
        // related: attachments.filter(attachment => !!attachment.cid)
        attached: [],
        related: []
      };

  }

  /**
   * List alternatives. Resulting objects can be used as input for MimeNode nodes
   *
   * @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
   */
  getAlternatives() {
    let alternatives = [],
      text,
      html,
      watchHtml,
      amp,
      icalEvent,
      eventObject;

    if (this.mail.text) {
      if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
        text = this.mail.text;
      } else {
        text = {
          content: this.mail.text
        };
      }
      text.contentType = 'text/plain; charset=utf-8';
    }

    if (this.mail.watchHtml) {
      if (
        typeof this.mail.watchHtml === 'object' &&
        (this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)
      ) {
        watchHtml = this.mail.watchHtml;
      } else {
        watchHtml = {
          content: this.mail.watchHtml
        };
      }
      watchHtml.contentType = 'text/watch-html; charset=utf-8';
    }

    if (this.mail.amp) {
      if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) {
        amp = this.mail.amp;
      } else {
        amp = {
          content: this.mail.amp
        };
      }
      amp.contentType = 'text/x-amp-html; charset=utf-8';
    }

    // only include the calendar alternative if there are no attachments
    // otherwise you might end up in a blank screen on some clients
    if (this.mail.icalEvent && !(this.mail.attachments && this.mail.attachments.length)) {
      if (
        typeof this.mail.icalEvent === 'object' &&
        (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
      ) {
        icalEvent = this.mail.icalEvent;
      } else {
        icalEvent = {
          content: this.mail.icalEvent
        };
      }

      eventObject = {};
      Object.keys(icalEvent).forEach(key => {
        eventObject[key] = icalEvent[key];
      });

      if (eventObject.content && typeof eventObject.content === 'object') {
        // we are going to have the same attachment twice, so mark this to be
        // resolved just once
        eventObject.content._resolve = true;
      }

      eventObject.filename = false;
      eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
      if (!eventObject.headers) {
        eventObject.headers = {};
      }
    }

    if (this.mail.html) {
      if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
        html = this.mail.html;
      } else {
        html = {
          content: this.mail.html
        };
      }
      html.contentType = 'text/html; charset=utf-8';
    }

    []
      .concat(text || [])
      .concat(watchHtml || [])
      .concat(amp || [])
      .concat(html || [])
      .concat(eventObject || [])
      .concat(this.mail.alternatives || [])
      .forEach(alternative => {
        let data;

        if (/^data:/i.test(alternative.path || alternative.href)) {
          alternative = this._processDataUrl(alternative);
        }

        data = {
          contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
          contentTransferEncoding: alternative.contentTransferEncoding
        };

        if (alternative.filename) {
          data.filename = alternative.filename;
        }

        if (/^https?:\/\//i.test(alternative.path)) {
          alternative.href = alternative.path;
          alternative.path = undefined;
        }

        if (alternative.raw) {
          data.raw = alternative.raw;
        } else if (alternative.path) {
          data.content = {
            path: alternative.path
          };
        } else if (alternative.href) {
          data.content = {
            href: alternative.href
          };
        } else {
          data.content = alternative.content || '';
        }

        if (alternative.encoding) {
          data.encoding = alternative.encoding;
        }

        if (alternative.headers) {
          data.headers = alternative.headers;
        }

        alternatives.push(data);
      });

    return alternatives;
  }

  /**
   * Builds multipart/mixed node. It should always contain different type of elements on the same level
   * eg. text + attachments
   *
   * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
   * @returns {Object} MimeNode node element
   */
  _createMixed(parentNode) {
    let node;

    if (!parentNode) {
      node = new MimeNode('multipart/mixed', {
        baseBoundary: this.mail.baseBoundary,
        textEncoding: this.mail.textEncoding,
        boundaryPrefix: this.mail.boundaryPrefix,
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess,
        normalizeHeaderKey: this.mail.normalizeHeaderKey
      });
    } else {
      node = parentNode.createChild('multipart/mixed', {
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess,
        normalizeHeaderKey: this.mail.normalizeHeaderKey
      });
    }

    if (this._useAlternative) {
      this._createAlternative(node);
    } else if (this._useRelated) {
      this._createRelated(node);
    }

    []
      .concat((!this._useAlternative && this._alternatives) || [])
      .concat(this._attachments.attached || [])
      .forEach(element => {
        // if the element is a html node from related subpart then ignore it
        if (!this._useRelated || element !== this._htmlNode) {
          this._createContentNode(node, element);
        }
      });

    return node;
  }

  /**
   * Builds multipart/alternative node. It should always contain same type of elements on the same level
   * eg. text + html view of the same data
   *
   * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
   * @returns {Object} MimeNode node element
   */
  _createAlternative(parentNode) {
    let node;

    if (!parentNode) {
      node = new MimeNode('multipart/alternative', {
        baseBoundary: this.mail.baseBoundary,
        textEncoding: this.mail.textEncoding,
        boundaryPrefix: this.mail.boundaryPrefix,
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess,
        normalizeHeaderKey: this.mail.normalizeHeaderKey
      });
    } else {
      node = parentNode.createChild('multipart/alternative', {
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess,
        normalizeHeaderKey: this.mail.normalizeHeaderKey
      });
    }

    this._alternatives.forEach(alternative => {
      if (this._useRelated && this._htmlNode === alternative) {
        this._createRelated(node);
      } else {
        this._createContentNode(node, alternative);
      }
    });

    return node;
  }

  /**
   * Builds multipart/related node. It should always contain html node with related attachments
   *
   * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
   * @returns {Object} MimeNode node element
   */
  _createRelated(parentNode) {
    let node;

    if (!parentNode) {
      node = new MimeNode('multipart/related; type="text/html"', {
        baseBoundary: this.mail.baseBoundary,
        textEncoding: this.mail.textEncoding,
        boundaryPrefix: this.mail.boundaryPrefix,
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess,
        normalizeHeaderKey: this.mail.normalizeHeaderKey
      });
    } else {
      node = parentNode.createChild('multipart/related; type="text/html"', {
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess,
        normalizeHeaderKey: this.mail.normalizeHeaderKey
      });
    }

    this._createContentNode(node, this._htmlNode);

    this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));

    return node;
  }

  /**
   * Creates a regular node with contents
   *
   * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
   * @param {Object} element Node data
   * @returns {Object} MimeNode node element
   */
  _createContentNode(parentNode, element) {
    element = element || {};
    element.content = element.content || '';

    let node;
    let encoding = (element.encoding || 'utf8')
      .toString()
      .toLowerCase()
      .replace(/[-_\s]/g, '');

    if (!parentNode) {
      node = new MimeNode(element.contentType, {
        filename: element.filename,
        baseBoundary: this.mail.baseBoundary,
        textEncoding: this.mail.textEncoding,
        boundaryPrefix: this.mail.boundaryPrefix,
        disableUrlAccess: this.mail.disableUrlAccess,
        disableFileAccess: this.mail.disableFileAccess
      });
    }
    // else {
    //   node = parentNode.createChild(element.contentType, {
    //     filename: element.filename,
    //     disableUrlAccess: this.mail.disableUrlAccess,
    //     disableFileAccess: this.mail.disableFileAccess,
    //     normalizeHeaderKey: this.mail.normalizeHeaderKey
    //   });
    // }

    // add custom headers
    // if (element.headers) {
    //   node.addHeader(element.headers);
    // }

    // if (element.cid) {
    //   node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
    // }
    //
    // if (element.contentTransferEncoding) {
    //   node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
    // } else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
    //   node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
    // }
    //
    // if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
    //   node.setHeader('Content-Disposition', element.contentDisposition || (element.cid ? 'inline' : 'attachment'));
    // }
    //
    // if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
    //   element.content = Buffer.from(element.content, encoding);
    // }

    // prefer pregenerated raw content
    // if (element.raw) {
    //   node.setRaw(element.raw);
    // } else {
      node.setContent(element.content);
    // }

    return node;
  }

  /**
   * Parses data uri and converts it to a Buffer
   *
   * @param {Object} element Content element
   * @return {Object} Parsed element
   */
  _processDataUrl(element) {
    let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
    if (!parts) {
      return element;
    }

    element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]));

    if ('path' in element) {
      element.path = false;
    }

    if ('href' in element) {
      element.href = false;
    }

    parts[1].split(';').forEach(item => {
      if (/^\w+\/[^/]+$/i.test(item)) {
        element.contentType = element.contentType || item.toLowerCase();
      }
    });

    return element;
  }
}

module.exports = MailComposer;
